package org.checkerframework.framework.type;

import com.sun.source.tree.ClassTree;
import com.sun.source.tree.MethodTree;
import com.sun.source.tree.Tree;
import com.sun.source.tree.TypeParameterTree;
import com.sun.source.tree.VariableTree;
import com.sun.tools.javac.code.Attribute;
import com.sun.tools.javac.code.Attribute.TypeCompound;
import com.sun.tools.javac.code.Symbol;
import com.sun.tools.javac.code.Symbol.MethodSymbol;
import com.sun.tools.javac.code.Symbol.VarSymbol;
import com.sun.tools.javac.code.Type;
import com.sun.tools.javac.code.TypeAnnotationPosition;
import com.sun.tools.javac.code.TypeAnnotationPosition.TypePathEntry;
import com.sun.tools.javac.code.TypeAnnotationPosition.TypePathEntryKind;
import com.sun.tools.javac.tree.JCTree;
import com.sun.tools.javac.util.List;
import com.sun.tools.javac.util.ListBuffer;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.type.TypeKind;
import javax.lang.model.util.Types;
import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedArrayType;
import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedDeclaredType;
import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedExecutableType;
import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedIntersectionType;
import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedPrimitiveType;
import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedTypeVariable;
import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedUnionType;
import org.checkerframework.framework.type.AnnotatedTypeMirror.AnnotatedWildcardType;
import org.checkerframework.framework.type.visitor.AnnotatedTypeScanner;
import org.checkerframework.javacutil.BugInCF;
import org.checkerframework.javacutil.TreeUtils;
import org.checkerframework.javacutil.TypeAnnotationUtils;

/**
 * A helper class that puts the annotations from an AnnotatedTypeMirrors back into the corresponding
 * Elements, so that they get stored in the bytecode by the compiler.
 *
 * <p>This has kind-of the symmetric function to {@code TypeFromElement}.
 *
 * <p>This class deals with javac internals and liberally imports such classes.
 */
public final class TypesIntoElements {

  /** Do not instantiate. */
  private TypesIntoElements() {
    throw new AssertionError("Class TypesIntoElements cannot be instantiated.");
  }

  /**
   * The entry point.
   *
   * @param processingEnv the environment
   * @param atypeFactory the type factory
   * @param tree the ClassTree to process
   */
  public static void store(
      ProcessingEnvironment processingEnv, AnnotatedTypeFactory atypeFactory, ClassTree tree) {
    Symbol.ClassSymbol csym = (Symbol.ClassSymbol) TreeUtils.elementFromDeclaration(tree);
    Types types = processingEnv.getTypeUtils();

    storeTypeParameters(processingEnv, types, atypeFactory, tree.getTypeParameters(), csym);

    /* TODO: storing extends/implements types results in
     * a strange error e.g. from the Nullness Checker.
     * I think somewhere we take the annotations on extends/implements as
     * the receiver annotation on a constructor, breaking logic there.
     * I assume that the problem is the default that we use for these locations.
     * Once we've decided the defaulting, enable this.
     * See example of code that fails when this is enabled in
     * checker/jtreg/nullness/annotationsOnExtends. Also, see
     * https://github.com/typetools/checker-framework/pull/876 for
     * a better implementation (though it also causes the error).
    storeClassExtends(processingEnv, types, atypeFactory, tree.getExtendsClause(), csym, -1);
    {
        int implidx = 0;
        for (Tree imp : tree.getImplementsClause()) {
            storeClassExtends(processingEnv, types, atypeFactory, imp, csym, implidx);
            ++implidx;
        }
    }
    */

    for (Tree mem : tree.getMembers()) {
      if (mem instanceof MethodTree) {
        storeMethod(processingEnv, types, atypeFactory, (MethodTree) mem);
      } else if (mem instanceof VariableTree) {
        storeVariable(processingEnv, types, atypeFactory, (VariableTree) mem);
      } else {
        // System.out.println("Unhandled member tree: " + mem);
      }
    }
  }

  private static void storeMethod(
      ProcessingEnvironment processingEnv,
      Types types,
      AnnotatedTypeFactory atypeFactory,
      MethodTree meth) {
    AnnotatedExecutableType mtype = atypeFactory.getAnnotatedType(meth);
    MethodSymbol sym = (MethodSymbol) TreeUtils.elementFromDeclaration(meth);
    TypeAnnotationPosition tapos;
    List<Attribute.TypeCompound> tcs = List.nil();

    storeTypeParameters(processingEnv, types, atypeFactory, meth.getTypeParameters(), sym);

    {
      // return type
      JCTree ret = ((JCTree.JCMethodDecl) meth).getReturnType();
      if (ret != null) {
        tapos = TypeAnnotationUtils.methodReturnTAPosition(ret.pos);
        tcs = tcs.appendList(generateTypeCompounds(processingEnv, mtype.getReturnType(), tapos));
      }
    }
    {
      // receiver
      JCTree receiverTree = ((JCTree.JCMethodDecl) meth).getReceiverParameter();
      if (receiverTree != null) {
        tapos = TypeAnnotationUtils.methodReceiverTAPosition(receiverTree.pos);
        tcs = tcs.appendList(generateTypeCompounds(processingEnv, mtype.getReceiverType(), tapos));
      }
    }
    {
      // parameters
      int pidx = 0;
      java.util.List<AnnotatedTypeMirror> ptypes = mtype.getParameterTypes();
      for (JCTree param : ((JCTree.JCMethodDecl) meth).getParameters()) {
        tapos = TypeAnnotationUtils.methodParameterTAPosition(pidx, param.pos);
        tcs = tcs.appendList(generateTypeCompounds(processingEnv, ptypes.get(pidx), tapos));
        ++pidx;
      }
    }
    {
      // throws clauses
      int tidx = 0;
      java.util.List<AnnotatedTypeMirror> ttypes = mtype.getThrownTypes();
      for (JCTree thr : ((JCTree.JCMethodDecl) meth).getThrows()) {
        tapos = TypeAnnotationUtils.methodThrowsTAPosition(tidx, thr.pos);
        tcs = tcs.appendList(generateTypeCompounds(processingEnv, ttypes.get(tidx), tapos));
        ++tidx;
      }
    }

    addUniqueTypeCompounds(types, sym, tcs);
  }

  private static void storeVariable(
      ProcessingEnvironment processingEnv,
      Types types,
      AnnotatedTypeFactory atypeFactory,
      VariableTree var) {
    VarSymbol sym = (VarSymbol) TreeUtils.elementFromDeclaration(var);
    AnnotatedTypeMirror type;
    if (atypeFactory instanceof GenericAnnotatedTypeFactory) {
      // TODO: this is rather ugly: we do not want refinement from the
      // initializer of the field. We need a general way to get
      // the "defaulted" type of a variable.
      type = ((GenericAnnotatedTypeFactory<?, ?, ?, ?>) atypeFactory).getAnnotatedTypeLhs(var);
    } else {
      type = atypeFactory.getAnnotatedType(var);
    }

    TypeAnnotationPosition tapos = TypeAnnotationUtils.fieldTAPosition(((JCTree) var).pos);

    List<Attribute.TypeCompound> tcs;
    tcs = generateTypeCompounds(processingEnv, type, tapos);
    addUniqueTypeCompounds(types, sym, tcs);
  }

  @SuppressWarnings("unused") // TODO: see usage in comments above
  private static void storeClassExtends(
      ProcessingEnvironment processingEnv,
      Types types,
      AnnotatedTypeFactory atypeFactory,
      Tree ext,
      Symbol.ClassSymbol csym,
      int implidx) {

    AnnotatedTypeMirror type;
    int pos;
    if (ext == null) {
      // The implicit superclass is always java.lang.Object.
      // TODO: is this a good way to get the type?
      type = atypeFactory.fromElement(csym.getSuperclass().asElement());
      pos = -1;
    } else {
      type = atypeFactory.getAnnotatedTypeFromTypeTree(ext);
      pos = ((JCTree) ext).pos;
    }

    TypeAnnotationPosition tapos = TypeAnnotationUtils.classExtendsTAPosition(implidx, pos);

    List<Attribute.TypeCompound> tcs;
    tcs = generateTypeCompounds(processingEnv, type, tapos);
    addUniqueTypeCompounds(types, csym, tcs);
  }

  private static void storeTypeParameters(
      ProcessingEnvironment processingEnv,
      Types types,
      AnnotatedTypeFactory atypeFactory,
      java.util.List<? extends TypeParameterTree> tps,
      Symbol sym) {
    boolean isClassOrInterface = sym.getKind().isClass() || sym.getKind().isInterface();
    List<Attribute.TypeCompound> tcs = List.nil();

    int tpidx = 0;
    for (TypeParameterTree tp : tps) {
      AnnotatedTypeVariable typeVar =
          (AnnotatedTypeVariable) atypeFactory.getAnnotatedTypeFromTypeTree(tp);
      // System.out.println("The Type for type parameter " + tp + " is " + type);

      TypeAnnotationPosition tapos;
      // Note: we use the type parameter pos also for the bounds;
      // the bounds may not be explicit and we couldn't look up separate pos.
      if (isClassOrInterface) {
        tapos = TypeAnnotationUtils.typeParameterTAPosition(tpidx, ((JCTree) tp).pos);
      } else {
        tapos = TypeAnnotationUtils.methodTypeParameterTAPosition(tpidx, ((JCTree) tp).pos);
      }

      { // This block is essentially direct annotations, perhaps we should refactor that
        // method out
        List<Attribute.TypeCompound> res = List.nil();
        for (AnnotationMirror am : typeVar.getLowerBound().getPrimaryAnnotations()) {
          Attribute.TypeCompound tc =
              TypeAnnotationUtils.createTypeCompoundFromAnnotationMirror(am, tapos, processingEnv);
          res = res.prepend(tc);
        }
        tcs = tcs.appendList(res);
      }

      AnnotatedTypeMirror tpbound = typeVar.getUpperBound();
      java.util.List<? extends AnnotatedTypeMirror> bounds;
      if (tpbound.getKind() == TypeKind.INTERSECTION) {
        bounds = ((AnnotatedIntersectionType) tpbound).getBounds();
      } else {
        bounds = List.of(tpbound);
      }

      int bndidx = 0;
      for (AnnotatedTypeMirror bound : bounds) {
        if (bndidx == 0 && ((Type) bound.getUnderlyingType()).isInterface()) {
          // If the first bound is an interface, there is an implicit java.lang.Object
          ++bndidx;
        }

        if (isClassOrInterface) {
          tapos =
              TypeAnnotationUtils.typeParameterBoundTAPosition(tpidx, bndidx, ((JCTree) tp).pos);
        } else {
          tapos =
              TypeAnnotationUtils.methodTypeParameterBoundTAPosition(
                  tpidx, bndidx, ((JCTree) tp).pos);
        }

        tcs = tcs.appendList(generateTypeCompounds(processingEnv, bound, tapos));
        ++bndidx;
      }
      ++tpidx;
    }

    // System.out.println("Adding " + tcs + " to " + sym);
    addUniqueTypeCompounds(types, sym, tcs);
  }

  private static void addUniqueTypeCompounds(Types types, Symbol sym, List<TypeCompound> tcs) {
    List<TypeCompound> raw = sym.getRawTypeAttributes();
    List<Attribute.TypeCompound> res = List.nil();

    for (Attribute.TypeCompound tc : tcs) {
      if (!TypeAnnotationUtils.isTypeCompoundContained(raw, tc, types)) {
        res = res.append(tc);
      }
    }
    // That method only uses reference equality. isTypeCompoundContained does a deep comparison.
    sym.appendUniqueTypeAttributes(res);
  }

  // Do not return null.  Return List.nil() if there are no TypeCompounds to return.
  private static List<Attribute.TypeCompound> generateTypeCompounds(
      ProcessingEnvironment processingEnv, AnnotatedTypeMirror type, TypeAnnotationPosition tapos) {
    return new TCConvert(processingEnv).scan(type, tapos);
  }

  /**
   * Convert an AnnotatedTypeMirror and a TypeAnnotationPosition into the corresponding
   * TypeCompounds.
   */
  private static class TCConvert
      extends AnnotatedTypeScanner<List<Attribute.TypeCompound>, TypeAnnotationPosition> {

    /** The processing environment. */
    private final ProcessingEnvironment processingEnv;

    /**
     * Creates a {@link TCConvert}.
     *
     * @param processingEnv the processing environment
     */
    TCConvert(ProcessingEnvironment processingEnv) {
      super(List.nil());
      this.processingEnv = processingEnv;
    }

    @Override
    public List<TypeCompound> scan(AnnotatedTypeMirror type, TypeAnnotationPosition pos) {
      if (pos == null) {
        throw new BugInCF("TypesIntoElements: invalid usage, null pos with type: " + type);
      }
      List<TypeCompound> res = super.scan(type, pos);
      return res;
    }

    @Override
    public List<TypeCompound> reduce(List<TypeCompound> r1, List<TypeCompound> r2) {
      if (r1 == null) {
        return r2;
      }
      if (r2 == null) {
        return r1;
      }
      return r1.appendList(r2);
    }

    private List<TypeCompound> directAnnotations(
        AnnotatedTypeMirror type, TypeAnnotationPosition tapos) {
      List<Attribute.TypeCompound> res = List.nil();

      for (AnnotationMirror am : type.getPrimaryAnnotations()) {
        // TODO: I BELIEVE THIS ISN'T TRUE BECAUSE PARAMETERS MAY HAVE ANNOTATIONS THAT CAME
        // FROM THE ELEMENT OF THE CLASS WHICH PREVIOUSLY WAS WRITTEN OUT BY
        // TYPESINTOELEMENT.
        //                if (am instanceof Attribute.TypeCompound) {
        //                    // If it is a TypeCompound it was already present in source
        // (right?),
        //                    // so there is nothing to do.
        //                    // System.out.println("  found TypeComound: " + am + " pos: "
        // + ((Attribute.TypeCompound)am).position);
        //                } else {
        // TODO: DOES THIS LEAD TO DOUBLING UP ON THE SAME ANNOTATION IN THE ELEMENT?
        Attribute.TypeCompound tc =
            TypeAnnotationUtils.createTypeCompoundFromAnnotationMirror(am, tapos, processingEnv);
        res = res.prepend(tc);
        //                }
      }
      return res;
    }

    @Override
    public List<TypeCompound> visitDeclared(
        AnnotatedDeclaredType type, TypeAnnotationPosition tapos) {
      if (visitedNodes.containsKey(type)) {
        return visitedNodes.get(type);
      }
      // Hack for termination
      visitedNodes.put(type, List.nil());
      List<Attribute.TypeCompound> res;

      TypeAnnotationPosition oldpos = TypeAnnotationUtils.copyTAPosition(tapos);
      locateNestedTypes(type, tapos);

      res = directAnnotations(type, tapos);

      // We sometimes fix-up raw types with wildcards.  Do not write these into the bytecode
      // as there are no corresponding type arguments and therefore no location to actually
      // add them to.
      if (!type.isUnderlyingTypeRaw()) {
        int arg = 0;
        for (AnnotatedTypeMirror ta : type.getTypeArguments()) {
          TypeAnnotationPosition newpos = TypeAnnotationUtils.copyTAPosition(tapos);
          newpos.location =
              tapos.location.append(new TypePathEntry(TypePathEntryKind.TYPE_ARGUMENT, arg));
          res = scanAndReduce(ta, newpos, res);
          ++arg;
        }
      }

      AnnotatedTypeMirror encl = type.getEnclosingType();
      if (encl != null && encl.getKind() != TypeKind.NONE && encl.getKind() != TypeKind.ERROR) {
        // use original tapos
        res = scanAndReduce(encl, oldpos, res);
      }
      visitedNodes.put(type, res);
      return res;
    }

    /**
     * Modeled after {@code
     * com.sun.tools.javac.code.TypeAnnotations.TypeAnnotationPositions#locateNestedTypes(Type,
     * TypeAnnotationPosition)}.
     *
     * @param type a type
     * @param p the type's position
     */
    private void locateNestedTypes(AnnotatedDeclaredType type, TypeAnnotationPosition p) {
      // The number of "steps" to get from the full type to the
      // left-most outer type.
      ListBuffer<TypePathEntry> depth = new ListBuffer<>();

      Type encl = (Type) type.getUnderlyingType().getEnclosingType();
      while (encl != null && encl.getKind() != TypeKind.NONE && encl.getKind() != TypeKind.ERROR) {
        depth = depth.append(TypePathEntry.INNER_TYPE);
        encl = encl.getEnclosingType();
      }

      if (depth.nonEmpty()) {
        p.location = p.location.appendList(depth.toList());
      }
    }

    @Override
    public List<TypeCompound> visitIntersection(
        AnnotatedIntersectionType type, TypeAnnotationPosition tapos) {
      if (visitedNodes.containsKey(type)) {
        return visitedNodes.get(type);
      }
      visitedNodes.put(type, List.nil());
      List<Attribute.TypeCompound> res;
      res = directAnnotations(type, tapos);

      int arg = 0;
      for (AnnotatedTypeMirror bound : type.getBounds()) {
        TypeAnnotationPosition newpos = TypeAnnotationUtils.copyTAPosition(tapos);
        newpos.location =
            tapos.location.append(new TypePathEntry(TypePathEntryKind.TYPE_ARGUMENT, arg));
        res = scanAndReduce(bound, newpos, res);
        ++arg;
      }
      visitedNodes.put(type, res);
      return res;
    }

    @Override
    public List<TypeCompound> visitUnion(AnnotatedUnionType type, TypeAnnotationPosition tapos) {
      // We should never need to write a union type, so raise an error.
      throw new BugInCF(
          "TypesIntoElement: encountered union type: " + type + " at position: " + tapos);
    }

    @Override
    public List<TypeCompound> visitArray(AnnotatedArrayType type, TypeAnnotationPosition tapos) {
      List<Attribute.TypeCompound> res;
      res = directAnnotations(type, tapos);

      TypeAnnotationPosition newpos = TypeAnnotationUtils.copyTAPosition(tapos);
      newpos.location = tapos.location.append(TypePathEntry.ARRAY);

      return reduce(super.visitArray(type, newpos), res);
    }

    @Override
    public List<TypeCompound> visitPrimitive(
        AnnotatedPrimitiveType type, TypeAnnotationPosition tapos) {
      List<Attribute.TypeCompound> res;
      res = directAnnotations(type, tapos);
      return res;
    }

    @Override
    public List<TypeCompound> visitTypeVariable(
        AnnotatedTypeVariable type, TypeAnnotationPosition tapos) {
      List<Attribute.TypeCompound> res;
      res = directAnnotations(type, tapos);
      // Do not call super. The bound will be visited separately.
      return res;
    }

    @Override
    public List<TypeCompound> visitWildcard(
        AnnotatedWildcardType type, TypeAnnotationPosition tapos) {
      if (this.visitedNodes.containsKey(type)) {
        return List.nil();
      }
      // Hack for termination, otherwise we'll visit one type too far (the same recursive
      // wildcard twice and generate extra type annos)
      visitedNodes.put(type, List.nil());
      List<Attribute.TypeCompound> res;

      // Note: By default, an Unbound wildcard will return true for both isExtendsBound and
      // isSuperBound
      if (((Type.WildcardType) type.getUnderlyingType()).isExtendsBound()) {
        res = directAnnotations(type.getSuperBound(), tapos);

        AnnotatedTypeMirror ext = type.getExtendsBound();
        if (ext != null) {
          TypeAnnotationPosition newpos = TypeAnnotationUtils.copyTAPosition(tapos);
          newpos.location = tapos.location.append(TypePathEntry.WILDCARD);
          res = scanAndReduce(ext, newpos, res);
        }

      } else {
        res = directAnnotations(type.getExtendsBound(), tapos);
        AnnotatedTypeMirror sup = type.getSuperBoundField();
        if (sup != null) {
          TypeAnnotationPosition newpos = TypeAnnotationUtils.copyTAPosition(tapos);
          newpos.location = tapos.location.append(TypePathEntry.WILDCARD);
          res = scanAndReduce(sup, newpos, res);
        }
      }
      visitedNodes.put(type, res);
      return res;
    }
  }
}
